iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Modern Web

Angular 深入淺出三十天:表單與測試系列 第 23

Angular 深入淺出三十天:表單與測試 Day23 - Reactive Forms 進階技巧 - 欄位連動檢核邏輯

  • 分享至 

  • xImage
  •  

Day23

大家在日常生活中,應該看過滿多表單的某個欄位會隨著另個欄位的改變,而造成該欄位的驗證邏輯需要改變的情況吧?

舉例來說,可能會有個欄位叫做聯絡資訊,使用者可以選擇要填入手機號碼或者是 E-mail ,該欄位再根據使用所選擇的類型來檢核該欄位的值。

今天,我們就來用 Reactive Forms 實作這個欄位,而這個欄位我會實作在我們的被保人表單上,各位就隨意吧!

如果已經忘記被保人表單長怎麼樣的話,可以先回頭複習一下第十一天的文章:Reactive Forms 實作 - 動態表單初體驗

實作開始

首先,我們需要在原本的被保人表單裡新增一個欄位:聯絡資訊。

HTML 的部份大概會長這樣:

<p>
  <label>聯絡資訊:</label>
</p>
<p>
  <select>
    <option value="">請選擇</option>
    <option value="mobile">手機</option>
    <option value="email">E-Mail</option>
  </select>
  <input type="text">
</p>

畫面看起來會像這樣:

Insured View

雖然聯絡資訊是一個欄位,但其實我們需要兩個 FormControl ,一個給下拉選單,一個給實際填值的 input 元素。

因此,我們要在原本的 createInsuredFormGroup 裡多加兩個欄位,像是這樣:

private createInsuredFormGroup(): FormGroup {
  return this.formBuilder.group({
    name: [
      '',
      [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
    ],
    gender: ['', Validators.required],
    age: ['', Validators.required],
    contactInfoType: ['', Validators.required],
    contactInfo: ['', Validators.required]
  });
}

然後將剛剛新增的欄位與畫面的元素綁定:

<p>
  <select formControlName="contactInfoType">
    <option value="">請選擇</option>
    <option value="mobile">手機</option>
    <option value="email">E-Mail</option>
  </select>
  <input type="text" formControlName="contactInfo">
</p>

接著我們透過把資料印在畫面上的方式來檢查是否已正確綁定,像這樣:

<pre>{{ formGroup?.getRawValue() | json }}</pre>

結果:

Insured View

看起來已經有正確跟畫面上的元素綁定了,那接下來要怎麼做才好呢?

valueChanges

FormControl 的父類別 AbstractControl 有個屬性叫做 valueChanges ,它是一個 Observable

我們可以透過訂閱某個 AbstractControlvalueChanges 這個 Observable 來知道該欄位是否已經發生變化,並且做出相應的處理。

因此,我們可以這樣調整 createInsuredFormGroup 裡的實作:

private createInsuredFormGroup(): FormGroup {
  const contactInfoTypeControl = this.formBuilder.control('', Validators.required);
  const contactInfoControl = this.formBuilder.control('', Validators.required);
  contactInfoTypeControl.valueChanges.subscribe((value) => {
    switch (value) {
      case 'mobile':
        contactInfoControl.setValidators([Validators.required, Validators.pattern(/^09\d{8}$/)]);  
        break;
      case 'email':
        contactInfoControl.setValidators([Validators.required, Validators.email]);  
        break;
      default:
        contactInfoControl.setValidators([Validators.required]);  
        break;
    }
    contactInfoControl.updateValueAndValidity();
  });

  return this.formBuilder.group({
    name: [
      '',
      [Validators.required, Validators.minLength(2), Validators.maxLength(10)]
    ],
    gender: ['', Validators.required],
    age: ['', Validators.required],
    contactInfoType: contactInfoTypeControl,
    contactInfo: contactInfoControl
  });
}

上述程式碼中有以下三個要點:

  1. 建立 FormControl 的時候可以藉由 this.formBuilder.control() 的方式建立,也可以直接使用 new FormControl() 建立,這點在前面的文章已經有提過,不過我在這邊再提醒大家一次。

  2. setValidators() 執行完後,記得一定要使用 updateValueAndValidity() 來更新當前欄位的驗證,不然就要等到該欄位的值有改變時才會以新的驗證器來驗證。

  3. 由於 contactInfoType 允許使用者選擇 請選擇 的選項,因此記得在 default 的區塊裡,將 Validators.required 給加回去。

這邊改好之後,我們也順便調整一下 getErrorMessage 的實作,讓使用者可以知道該欄位的驗證有誤:

getErrorMessage(key: string, index: number): string {
  const formGroup = this.formArray.controls[index];
  const formControl = formGroup.get(key);
  let errorMessage: string;
  if (!formControl || !formControl.errors || formControl.pristine) {
    errorMessage = '';
  } else if (formControl.errors.required) {
    errorMessage = '此欄位必填';
  } else if (formControl.errors.minlength) {
    errorMessage = '姓名至少需兩個字以上';
  } else if (formControl.errors.maxlength) {
    errorMessage = '姓名至多只能輸入十個字';

  // 增加以下兩個判斷  
  } else if (formControl.errors.pattern) {
    errorMessage = '手機號碼格式錯誤';
  } else if (formControl.errors.email) {
    errorMessage = 'E-mail 格式錯誤';
  }

  return errorMessage!;
}

這邊要提醒大家的是,由於驗證 E-mail 格式的方式我今天是用 Validators.email 的驗證器來驗,不是之前的 Validators.pattern() ,所以我可以直接用 formControl.errors.email 來判斷。

如果實作時,手機號碼跟 E-mail 都是用 Validators.pattern() 的驗證器來驗的話,就需要進一步去比對 formControl.errors.pattern 裡的 Regular Expression 來分辨究竟是手機號碼的格式錯誤還是 E-mail 的格式錯誤了。

像是這樣:

} else if (formControl.errors.pattern) {
  const requiredPattern = formControl.errors.pattern.requiredPattern;
  if (requiredPattern === '/A Regular Expression/') {
    errorMessage = '手機號碼格式錯誤';
  } else if (requiredPattern === '/B Regular Expression/') {
    errorMessage = 'E-mail 格式錯誤';
  }
}

如此一來,我們就完成這個欄位的功能囉!

結果:

Insured View

本日小結

今天的重點是學會如何使用 valueChanges 來動態調整相關欄位的驗證邏輯。

雖然是 Observable 是 RxJS 的東西,但今天並沒有太艱難或太複雜的運用,使用上的感覺會跟使用 Promise 的感覺類似,不過我個人認為 RxJS 好玩且強大許多。

關於 RxJS ,如果大家想知道更多資訊,我推薦大家去看 Mike 的打通 RxJS 任督二脈系列文,或者是直接買實體書也行。

雖然今天的實作已經完成了,但因為有調整程式碼的關係,測試程式碼其實也需要相應的調整才不會出錯,此部份就交給大家實作我就不再用篇幅分享實作囉!

今天的程式碼會放在 Github - Branch: day23 上供大家參考,建議大家在看我的實作之前,先按照需求規格自己做一遍,之後再跟我的對照,看看自己的實作跟我的實作不同的地方在哪裡、有什麼好處與壞處,如此反覆咀嚼消化後,我相信你一定可以進步地非常快!

如果有任何的問題或是回饋,還請麻煩留言給我讓我知道!


上一篇
Angular 深入淺出三十天:表單與測試 Day22 - 把 Cypress 變成 TypeScript 版
下一篇
Angular 深入淺出三十天:表單與測試 Day24 - Reactive Forms 進階技巧 - Auto-Complete Searching
系列文
Angular 深入淺出三十天:表單與測試30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ryan851109
iT邦新手 5 級 ‧ 2022-03-04 18:35:47

Hi Leo,
我在驗證電話號碼的程式碼上會驗證失敗
示意圖:
https://ithelp.ithome.com.tw/upload/images/20220304/20108518KaqZPqNIwJ.png
程式碼中的$^好像打反了

Leo iT邦新手 3 級 ‧ 2022-03-07 09:25:48 檢舉

噗,真的打反了!感謝糾正!!

我要留言

立即登入留言